using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Runtime.Serialization;
using System.Reflection;

using Pegasus.Diagnostics;
using Pegasus.Reflection;

namespace Pegasus.Runtime.Serialization
{
	/// <summary>
	/// This class implements a basic object binder that will look for the given object type
	/// in the following order.
	/// 1. Uses the full assembly name
	/// 2. Uses a version independate assembly name
	/// 3. Uses a non-culture assembly name
	/// 4. Uses a non-strong name assembly (basicly just the assembly name at this point)
    /// 5. Uses just the full class name and matches with an assembly by the same name in the loaded assemblies
	/// </summary>
	public class ObjectSerializationBinder : SerializationBinder
	{
		// Local Instance Values
		private Dictionary<string, Type> m_typeTable = new Dictionary<string, Type>();
		private bool m_unknownTypeReplacement = false;

		/// <summary>
		/// Initializes a new instance of the <see cref="T:ObjectSerializationBinder"/> class.
		/// </summary>
		public ObjectSerializationBinder()
		{
			// Add changes to the pegasus type serialize objects

			// 8/16/06 - Reworked workflow the workflow (again)
			AddTypeOverride( "Pegasus.Workflow.Service.WorkflowEventInfo", typeof( Pegasus.Workflow.Service.WorkflowMessageService.WorkflowEventMessage ) );
			AddTypeOverride( "Pegasus.Workflow.Service.WorkflowEventInfo[]", typeof( Pegasus.Workflow.Service.WorkflowMessageService.WorkflowEventMessage[] ) );
			AddTypeOverride( "Pegasus.Workflow.Service.WorkflowEventType", typeof( object ) );
			AddTypeOverride( "Pegasus.Workflow.Service.WorkflowEventType[]", typeof( object[] ) );

			// 7/21/06 - Change/Rename the event info types for workflow
			AddTypeOverride( "Pegasus.Workflow.Service.WorkflowContext+WorkflowCompletedEventInfo", typeof( Pegasus.Workflow.Service.WorkflowMessageService.WorkflowEventMessage ) );
			AddTypeOverride( "Pegasus.Workflow.Service.WorkflowContext+WorkflowCompletedEventInfo[]", typeof( Pegasus.Workflow.Service.WorkflowMessageService.WorkflowEventMessage[] ) );
			AddTypeOverride( "Pegasus.Workflow.Service.WorkflowContext+WorkflowCompletedBindingType", typeof( object ) );
			AddTypeOverride( "Pegasus.Workflow.Service.WorkflowContext+WorkflowCompletedBindingType[]", typeof( object[] ) );
		}

		/// <summary>
		/// Gets or sets a value indicating whether to use unknown type replacement.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if unknown type replacement is used; otherwise, <c>false</c>.
		/// </value>
		public bool UnknownTypeReplacement
		{
			get
			{
				return m_unknownTypeReplacement;
			}
			
			set
			{
				m_unknownTypeReplacement = value;
			}
		}

		/// <summary>
		/// When overridden in a derived class, controls the binding of a serialized object to a type.
		/// </summary>
		/// <param name="assemblyName">Specifies the <see cref="T:System.Reflection.Assembly"></see> name of the serialized object.</param>
		/// <param name="typeName">Specifies the <see cref="T:System.Type"></see> name of the serialized object.</param>
		/// <returns>
		/// The type of the object the formatter creates a new instance of.
		/// </returns>
		public override Type BindToType( string assemblyName, string typeName )
		{
			// Check parameters
			ParamCode.AssertNotEmpty( assemblyName, "assemblyName" );
			ParamCode.AssertNotEmpty( typeName, "typeName" );

			// Try to bind to the given type
			TypeName typeNameParam = new TypeName( assemblyName, typeName );
			return BindToGivenType( typeNameParam, m_unknownTypeReplacement );
		}

        /// <summary>
        /// Binds to type.
        /// </summary>
        /// <param name="typeName">Name of the type.</param>
        public Type BindToType( TypeName typeName )
        {
            // Check parameters
            ParamCode.AssertNotNull( typeName, "typeName" );

            // Try to bind to the given type
            return BindToGivenType( typeName, m_unknownTypeReplacement );
        }

		/// <summary>
		/// Adds the type override.
		/// </summary>
		/// <param name="oldTypeName">Old name of the type.</param>
		/// <param name="newType">The new type.</param>
		protected void AddTypeOverride( string oldTypeName, Type newType )
		{
			m_typeTable.Add( oldTypeName, newType );
		}

		/// <summary>
		/// Binds to type specific.
		/// </summary>
		/// <param name="typeName">Name of the type.</param>
		/// <param name="unknownTypeReplacement">if set to <c>true</c> to replace unknown types with Object.</param>
		/// <returns></returns>
		private Type BindToGivenType( TypeName typeName, bool unknownTypeReplacement )
		{
			Type type = null;

			// Check if we have an override type registered.
			if( m_typeTable.ContainsKey( typeName.FullTypeName ) )
			{
				return m_typeTable[ typeName.FullTypeName ];
			}

			// If this is a generic type then we need to bind the sub types first
			// and replace anything that is out of date.
			if( typeName.IsGenericType || typeName.IsArrayItemGenericType )
			{
				ReadOnlyCollection<TypeName> parametersCollection = typeName.GenericParameters;
				int count = parametersCollection.Count;
				TypeName[] newTypeNames = new TypeName[ count ];

				for( int x = 0; x < count; x++ )
				{
					// Get the sub-type, sub-types can not be replaced.
					Type subType = BindToGivenType( parametersCollection[ x ], false );
					if( subType != null )
					{
						newTypeNames[ x ] = new TypeName( subType.AssemblyQualifiedName );
					}
					else
					{
						throw new TypeLoadException( string.Format( "Unable to find generic sub-type {0}, {1}, from {2}", parametersCollection[ x ].FullTypeName, parametersCollection[ x ].FullyQualifiedAssemblyName, typeName.FullyQualifiedTypeName ) );
					}
				}

				typeName = typeName.ReplaceGenericParameters( newTypeNames );
			}

			// Now we have all the sub-types or we just have the given type we can 
			// try to match it.
			try
			{
				// Try for an exact match
				type = Type.GetType( typeName.FullyQualifiedTypeName );
				if( type == null )
				{
					// Now check without the version
					type = BindToTypeWithoutVersion( typeName, unknownTypeReplacement );
				}
			}
			catch( FileLoadException )
			{
				// Now check without the version
				type = BindToTypeWithoutVersion( typeName, unknownTypeReplacement );
			}

			return type;
		}

		/// <summary>
		/// Binds to type without version.
		/// </summary>
		/// <param name="typeName">Name of the type.</param>
		/// <param name="unknownTypeReplacement">if set to <c>true</c> to replace unknown types with Object.</param>
		/// <returns></returns>
		private Type BindToTypeWithoutVersion( TypeName typeName, bool unknownTypeReplacement )
		{
			Type type = null;

			try
			{
				// Try again without the version 
				type = Type.GetType( typeName.GetFullyQualifiedTypeName( TypeNameFilter.ExcludeVersion ) );
				if( type == null )
				{
					// Now check without the culture
					type = BindToTypeWithoutCulture( typeName, unknownTypeReplacement );
				}
			}
			catch( FileLoadException )
			{
				// Now check without the culture
				type = BindToTypeWithoutCulture( typeName, unknownTypeReplacement );
			}

			return type;
		}

		/// <summary>
		/// Binds to type without culture.
		/// </summary>
		/// <param name="typeName">Name of the type.</param>
		/// <param name="unknownTypeReplacement">if set to <c>true</c> to replace unknown types with Object.</param>
		/// <returns></returns>
		private Type BindToTypeWithoutCulture( TypeName typeName, bool unknownTypeReplacement )
		{
			Type type = null;

			try
			{
				// Try again with out the version or the culture
				type = Type.GetType( typeName.GetFullyQualifiedTypeName( TypeNameFilter.ExcludeVersion | TypeNameFilter.ExcludeCulture ) );
				if( type == null )
				{
					// Now check without the public key
					type = BindToTypeWithoutPublicKey( typeName, unknownTypeReplacement );
				}
			}
			catch( FileLoadException )
			{
				// Now check without the public key
				type = BindToTypeWithoutPublicKey( typeName, unknownTypeReplacement );
			}

			return type;
		}

		/// <summary>
		/// Binds to type without public key.
		/// </summary>
		/// <param name="typeName">Name of the type.</param>
		/// <param name="unknownTypeReplacement">if set to <c>true</c> to replace unknown types with Object.</param>
		/// <returns></returns>
		private Type BindToTypeWithoutPublicKey( TypeName typeName, bool unknownTypeReplacement )
		{
			Type type = null;

			try
			{
				// Try again with out the version, the culture, or the public key
				type = Type.GetType( typeName.GetFullyQualifiedTypeName( TypeNameFilter.ExcludeVersion | TypeNameFilter.ExcludeCulture | TypeNameFilter.ExcludePublicKey ) );
				if( type == null )
				{
                    // Next bind with just the type name and assembly name matching
                    type = BindToTypeWithoutAssembly( typeName, unknownTypeReplacement );
				}
			}
			catch( FileLoadException )
			{
                // Next bind with just the type name and assembly name matching
                type = BindToTypeWithoutAssembly( typeName, unknownTypeReplacement );
			}

			return type;
		}

        private Type BindToTypeWithoutAssembly( TypeName typeName, bool unknownTypeReplacement )
        {
            try
            {
                // Try again by matching the assembly name with a loaded assembly and type name likewise
                foreach ( Assembly assembly in AppDomain.CurrentDomain.GetAssemblies() )
                {
                    if ( assembly.FullName.StartsWith( typeName.AssemblyName ) )
                    {
                        Type type = assembly.GetType( typeName.FullTypeName );
                        if( type != null )
                        {
                            return type;
                        }
                        break;
                    }
                }
            }
            catch( FileLoadException ) {}

            if( unknownTypeReplacement )
            {
                return typeof( object );
            }
            else
            {
                return null;
            }
        }
	}
}
